#!/usr/bin/python2.4


import itertools
import time

import flags
from logger import logger
import utils


flags.add_option('--config-check-valid-time-in-hour', '-t',
                 type='float', default=1)
flags.add_option('--no-show-unusable-hosts', '--nosuh', action='store_false',
                 dest='show_unusable_hosts', default=True)
flags.add_option('--show-busy-hosts', '--sbh', action='store_true')
flags.add_option('--no-show-completed-hosts', '--nosch', action='store_false',
                 dest='show_completed_hosts', default=True)
flags.add_option('--no-show-triage-needed', '--nostn', action='store_false',
                 dest='show_triage_needed', default=True)
flags.add_option('--no-show-schedule-adjustment', '--nossa',
                 action='store_false', dest='show_schedule_adjustment',
                 default=True)
flags.add_option('--no-show-discarded-hosts', '--nosdh', action='store_false',
                 dest='show_discarded_hosts', default=True)
flags.add_option('--disable-schedule-adjustment', '--dsa', action='store_true')


class Scheduler(object):

  def __init__(self, test_results):
    self.test_results = test_results
    if test_results.test_plan.check_config:
      self.config_check = self.test_results.test_plan.tests[0]
    else:
      self.config_check = None
    self.hostnames_by_status = None
    self.triage_needed_runs = None
    self.hostnames_run_test = None

  def setup(self):
    pass

  def check_host_next_tests(self, host):
    state = host.state
    if state:
      return None, state[0]
    if not host.idle:
      return None, 'busy'

    pending_tests = []
    job_status = self.test_results.host_job_status[host.hostname]
    for test in self.test_results.test_plan.tests:
      status, job_host_run = job_status[test.job_name]
      if status == 'RUNNING':
        logger.error('Host %s is still running job %s' %
                     (host.hostname, job_host_run.job_tag))
        return None, 'running'
      if status == 'TRIAGE NEEDED' or status == 'TRIAGED':
        return None, job_host_run
      if status == 'PENDING':
        pending_tests.append(test)

    return pending_tests, ''

  def last_run_is_good_config_check(self, host, valid_time_in_hour=0):
    job_host_runs = self.test_results.host_results.get(host.hostname)
    if not job_host_runs:
      return False
    last_run = job_host_runs[-1]
    if (last_run.test_job_name != self.config_check.job_name or
        last_run.status != 'GOOD'):
      return False
    if valid_time_in_hour:
      if last_run.job_finished_time:
        finish_time = utils.parse_time(last_run.job_finished_time)
      else:
        logger.error('job_finished_time of %s is empty %r, '
                     'use test_finished_time %r instead' %
                     (last_run.job_tag,
                      last_run.job_finished_time,
                      last_run.test_finished_time))
        finish_time = utils.parse_time(last_run.test_finished_time)
      if time.time() - finish_time > valid_time_in_hour * 60 * 60:
        return False
    return True

  def determine_next_test_to_run(self, host, pending_tests):
    if not pending_tests:
      if (self.config_check and
          not self.last_run_is_good_config_check(host)):
          return self.config_check, 'Final'
      return None, 'completed'

    test = pending_tests[0]
    if (self.config_check and
        test is not self.config_check and
        not self.last_run_is_good_config_check(
            host, valid_time_in_hour=flags.get_option(
                'config_check_valid_time_in_hour', 1))):
      return self.config_check, "Pre '%s'" % test.job_name
    return test, ''

  def process_host(self, host):
    next_tests, reason_or_run = self.check_host_next_tests(host)
    if next_tests is None:
      if isinstance(reason_or_run, basestring):
        self.hostnames_by_status.setdefault(
            reason_or_run, []).append(host.hostname)
      else:
        self.triage_needed_runs.setdefault(
            reason_or_run.test_job_name, []).append(reason_or_run)
    else:
      next_test_to_run, reason = self.determine_next_test_to_run(
          host, next_tests)
      if next_test_to_run is None:
        self.hostnames_by_status.setdefault(
            reason, []).append(host.hostname)
      else:
        self.hostnames_run_test.setdefault(
            next_test_to_run.job_name, []).append(host.hostname)

  def log_hosts_with_status(self, status, level=logger.ERROR):
    hostnames = self.hostnames_by_status.get(status)
    if hostnames:
      logger.logging(level, 'Hosts with status %r: %s' %
                            (status, utils.pformat_array(hostnames)))
      return True
    return False

  def log_triage_needed_runs(self, level=logger.ERROR):
    results = []
    for test in self.test_results.test_plan.tests:
      job_host_runs = self.triage_needed_runs.get(test.job_name)
      if job_host_runs:
        results.append(" '%s' %s: %s" % (
            test.job_name, len(job_host_runs),
            ' '.join(run.job_tag for run in job_host_runs)))
    if results:
      logger.logging(level, 'Triage needed failures:\n%s' %
                            '\n'.join(results))
      return True
    return False

  def adjust_schedules(self):

    def good_to_run(test, l):
      return not (test and l % test.synch_count)

    def get_test_hostnames(test, none_host_status):
      if test:
        return self.hostnames_run_test.get(test.job_name, [])
      return self.hostnames_by_status.get(none_host_status, [])

    def is_valid_move(hostname, test1, test2):
      len1 = len(get_test_hostnames(test1, 'completed'))
      if good_to_run(test1, len1) and not good_to_run(test1, len1 - 1):
        return False
      len2 = len(get_test_hostnames(test2, 'discarded'))
      if good_to_run(test2, len2) and not good_to_run(test2, len2 + 1):
        return False
      if (test2 and hostname and
          self.test_results.host_job_status[
              hostname][test2.job_name][0] == 'N/A'):
        return False
      return True

    def run_score(test, hostname):
      if not test:
        return 0
      if not hostname:
        status, previous_run = 'PENDING', None
      else:
        status, previous_run = self.test_results.host_job_status[
            hostname][test.job_name]
      if status == 'PENDING':
        if test.synch_count > 1:
          return 1100
        return 1000
      if status == 'GOOD' and test.synch_count > 1:
        return 1
      return 0

    def get_score(hostname, test1, test2):
      if not is_valid_move(hostname, test1, test2):
        return -10000
      score = 0
      len1 = len(get_test_hostnames(test1, 'completed'))
      if not good_to_run(test1, len1) and good_to_run(test1, len1 - 1):
        score += 10
      if good_to_run(test1, len1):
        score -= run_score(test1, hostname)
      len2 = len(get_test_hostnames(test2, 'discarded'))
      if not good_to_run(test2, len2) and good_to_run(test2, len2 + 1):
        score += 10
        score += run_score(test2, None) * (len2 % test2.synch_count)
      if good_to_run(test2, len2 + 1):
        score += run_score(test2, hostname)
      return score

    def adjust_test(test):
      best = [0, None, None, None]

      def update_best(hostname, test1, test2):
        score = get_score(hostname, test1, test2)
        if score > best[0]:
          best[:] = score, hostname, test1, test2
          return True
        return False

      for hostname in self.hostnames_run_test[test.job_name]:
        for test2 in itertools.chain(
            [None], self.test_results.test_plan.tests):
          if test2 is not test:
            update_best(hostname, test, test2)

      for test1 in itertools.chain(
          [None], self.test_results.test_plan.tests):
        if test1 is not test and not (
            self.config_check and test1 is self.config_check):
          for hostname in get_test_hostnames(test1, 'completed'):
            update_best(hostname, test1, test)

      best_score, best_hostname, best_test1, best_test2 = best
      if best_score > 0:
        if best_test1:
          from_name = best_test1.job_name
          hostnames1 = self.hostnames_run_test[from_name]
          from_status = ' (%s)' % (
              self.test_results.host_job_status[best_hostname][from_name][0])
        else:
          from_name = 'completed'
          hostnames1 = self.hostnames_by_status[from_name]
          from_status = ''
        if best_test2:
          to_name = best_test2.job_name
          hostnames2 = self.hostnames_run_test.setdefault(to_name, [])
          to_status = ' (%s)' % (
              self.test_results.host_job_status[best_hostname][to_name][0])
        else:
          to_name = 'discarded'
          hostnames2 = self.hostnames_by_status.setdefault(to_name, [])
          to_status = ''

        if flags.get_option('show_schedule_adjustment', True):
          logger.error("Move %s from '%s'%s to '%s'%s, score %s" % (
                       best_hostname, from_name, from_status,
                       to_name, to_status, best_score))
        hostnames1.remove(best_hostname)
        hostnames2.append(best_hostname)

      return best_score

    def discard_hosts(test):
      hostnames = self.hostnames_run_test.get(test.job_name, [])
      remainder = len(hostnames) % test.synch_count
      if remainder:
        discarded_hostnames = self.hostnames_by_status.setdefault(
            'discarded', [])
        if flags.get_option('show_schedule_adjustment', True):
          logger.error("Discard %s from '%s'" %
                       (utils.pformat_array(hostnames[-remainder:]),
                        test.job_name))
          discarded_hostnames += hostnames[-remainder:]
          del hostnames[-remainder:]
      return remainder


    for test in self.test_results.test_plan.tests:
      while not good_to_run(
          test, len(self.hostnames_run_test.get(test.job_name, []))):
        if flags.get_option('disable_schedule_adjustment'):
          discard_hosts(test)
        elif adjust_test(test) <= 0:
          logger.error('Can not find any move, give up on %s' %
                       test.job_name)
          break

  def get_schedules(self):
    self.hostnames_by_status = {}
    self.triage_needed_runs = {}
    self.hostnames_run_test = {}

    for host in self.test_results.hosts:
      self.process_host(host)

    if flags.get_option('show_unusable_hosts', True):
      for status in ('hwops', 'free', 'locked', 'triage needed'):
        self.log_hosts_with_status(status)

    if flags.get_option('show_busy_hosts'):
      self.log_hosts_with_status('busy')

    if flags.get_option('show_completed_hosts'):
      self.log_hosts_with_status('completed')

    if flags.get_option('show_triage_needed', True):
      self.log_triage_needed_runs()

    self.adjust_schedules()

    if flags.get_option('show_discarded_hosts', True):
      self.log_hosts_with_status('discarded')

    return [(test.job_name, self.hostnames_run_test[test.job_name], None)
            for test in self.test_results.test_plan.tests
            if self.hostnames_run_test.get(test.job_name)]
